// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterMutatorView.h"

#include "third_party/googletest/googletest/include/gtest/gtest.h"

#import "flutter/shell/platform/darwin/macos/framework/Source/NSView+ClipsToBounds.h"

@interface FlutterMutatorView (Private)

@property(readonly, nonatomic, nonnull) NSMutableArray<NSView*>* pathClipViews;
@property(readonly, nonatomic, nullable) NSView* platformViewContainer;

@end

static constexpr float kMaxErr = 1e-10;

namespace {
void ApplyFlutterLayer(FlutterMutatorView* view,
                       FlutterSize size,
                       const std::vector<FlutterPlatformViewMutation>& mutations) {
  flutter::PlatformViewLayer layer(0,  // identifier
                                   mutations,
                                   // Offset is ignored by mutator view, the bounding rect is
                                   // determined by width and transform.
                                   FlutterPoint{0, 0},  // offset
                                   size);

  [view applyFlutterLayer:&layer];
}

// Expect that each element within two CATransform3Ds is within an error bound.
//
// In order to avoid architecture-specific floating point differences we don't check for exact
// equality using, for example, CATransform3DEqualToTransform.
void ExpectTransform3DEqual(const CATransform3D& t, const CATransform3D& u) {
  EXPECT_NEAR(t.m11, u.m11, kMaxErr);
  EXPECT_NEAR(t.m12, u.m12, kMaxErr);
  EXPECT_NEAR(t.m13, u.m13, kMaxErr);
  EXPECT_NEAR(t.m14, u.m14, kMaxErr);

  EXPECT_NEAR(t.m21, u.m21, kMaxErr);
  EXPECT_NEAR(t.m22, u.m22, kMaxErr);
  EXPECT_NEAR(t.m23, u.m23, kMaxErr);
  EXPECT_NEAR(t.m24, u.m24, kMaxErr);

  EXPECT_NEAR(t.m31, u.m31, kMaxErr);
  EXPECT_NEAR(t.m32, u.m32, kMaxErr);
  EXPECT_NEAR(t.m33, u.m33, kMaxErr);
  EXPECT_NEAR(t.m34, u.m34, kMaxErr);

  EXPECT_NEAR(t.m41, u.m41, kMaxErr);
  EXPECT_NEAR(t.m42, u.m42, kMaxErr);
  EXPECT_NEAR(t.m43, u.m43, kMaxErr);
  EXPECT_NEAR(t.m44, u.m44, kMaxErr);
}
}  // namespace

TEST(FlutterMutatorViewTest, BasicFrameIsCorrect) {
  NSView* platformView = [[NSView alloc] init];
  FlutterMutatorView* mutatorView = [[FlutterMutatorView alloc] initWithPlatformView:platformView];

  EXPECT_EQ(mutatorView.platformView, platformView);

  std::vector<FlutterPlatformViewMutation> mutations{
      {
          .type = kFlutterPlatformViewMutationTypeTransformation,
          .transformation =
              FlutterTransformation{
                  .scaleX = 1,
                  .transX = 100,
                  .scaleY = 1,
                  .transY = 50,
              },
      },
  };

  ApplyFlutterLayer(mutatorView, FlutterSize{30, 20}, mutations);

  EXPECT_TRUE(CGRectEqualToRect(mutatorView.frame, CGRectMake(100, 50, 30, 20)));
  EXPECT_TRUE(CGRectEqualToRect(platformView.frame, CGRectMake(0, 0, 30, 20)));
  EXPECT_EQ(mutatorView.pathClipViews.count, 0ull);
  EXPECT_NE(mutatorView.platformViewContainer, nil);
}

TEST(FlutterMutatorViewTest, ClipsToBounds) {
  NSView* platformView = [[NSView alloc] init];
  FlutterMutatorView* mutatorView = [[FlutterMutatorView alloc] initWithPlatformView:platformView];
  EXPECT_TRUE(mutatorView.clipsToBounds);
}

TEST(FlutterMutatorViewTest, TransformedFrameIsCorrect) {
  NSView* platformView = [[NSView alloc] init];
  FlutterMutatorView* mutatorView = [[FlutterMutatorView alloc] initWithPlatformView:platformView];
  NSView* mutatorViewParent = [[NSView alloc] init];
  mutatorViewParent.wantsLayer = YES;
  mutatorViewParent.layer.contentsScale = 2.0;
  [mutatorViewParent addSubview:mutatorView];

  std::vector<FlutterPlatformViewMutation> mutations{
      {
          .type = kFlutterPlatformViewMutationTypeTransformation,
          .transformation =
              FlutterTransformation{
                  .scaleX = 2,
                  .scaleY = 2,
              },
      },
      {
          .type = kFlutterPlatformViewMutationTypeTransformation,
          .transformation =
              FlutterTransformation{
                  .scaleX = 1,
                  .transX = 100,
                  .scaleY = 1,
                  .transY = 50,
              },
      },
      {
          .type = kFlutterPlatformViewMutationTypeTransformation,
          .transformation =
              FlutterTransformation{
                  .scaleX = 1.5,
                  .transX = -7.5,
                  .scaleY = 1.5,
                  .transY = -5,
              },
      },
  };

  // PlatformView size form engine comes in physical pixels
  ApplyFlutterLayer(mutatorView, FlutterSize{30 * 2, 20 * 2}, mutations);
  EXPECT_TRUE(CGRectEqualToRect(mutatorView.frame, CGRectMake(92.5, 45, 45, 30)));
  EXPECT_TRUE(CGRectEqualToRect(platformView.frame, CGRectMake(0, 0, 30, 20)));

  ExpectTransform3DEqual(mutatorView.platformViewContainer.layer.sublayerTransform,
                         CATransform3DMakeScale(1.5, 1.5, 1));
}

TEST(FlutterMutatorViewTest, FrameWithLooseClipIsCorrect) {
  NSView* platformView = [[NSView alloc] init];
  FlutterMutatorView* mutatorView = [[FlutterMutatorView alloc] initWithPlatformView:platformView];

  EXPECT_EQ(mutatorView.platformView, platformView);

  std::vector<FlutterPlatformViewMutation> mutations{
      {
          .type = kFlutterPlatformViewMutationTypeClipRect,
          .clip_rect = FlutterRect{80, 40, 200, 100},
      },
      {
          .type = kFlutterPlatformViewMutationTypeTransformation,
          .transformation =
              FlutterTransformation{
                  .scaleX = 1,
                  .transX = 100,
                  .scaleY = 1,
                  .transY = 50,
              },
      },
  };

  ApplyFlutterLayer(mutatorView, FlutterSize{30, 20}, mutations);

  EXPECT_TRUE(CGRectEqualToRect(mutatorView.frame, CGRectMake(100, 50, 30, 20)));
  EXPECT_TRUE(CGRectEqualToRect(platformView.frame, CGRectMake(0, 0, 30, 20)));
}

TEST(FlutterMutatorViewTest, FrameWithTightClipIsCorrect) {
  NSView* platformView = [[NSView alloc] init];
  FlutterMutatorView* mutatorView = [[FlutterMutatorView alloc] initWithPlatformView:platformView];

  EXPECT_EQ(mutatorView.platformView, platformView);

  std::vector<FlutterPlatformViewMutation> mutations{
      {
          .type = kFlutterPlatformViewMutationTypeClipRect,
          .clip_rect = FlutterRect{80, 40, 200, 100},
      },
      {
          .type = kFlutterPlatformViewMutationTypeClipRect,
          .clip_rect = FlutterRect{110, 55, 120, 65},
      },
      {
          .type = kFlutterPlatformViewMutationTypeTransformation,
          .transformation =
              FlutterTransformation{
                  .scaleX = 1,
                  .transX = 100,
                  .scaleY = 1,
                  .transY = 50,
              },
      },
  };

  ApplyFlutterLayer(mutatorView, FlutterSize{30, 20}, mutations);

  EXPECT_TRUE(CGRectEqualToRect(mutatorView.frame, CGRectMake(110, 55, 10, 10)));
  EXPECT_TRUE(
      CGRectEqualToRect(mutatorView.subviews.firstObject.frame, CGRectMake(-10, -5, 30, 20)));
  EXPECT_TRUE(CGRectEqualToRect(platformView.frame, CGRectMake(0, 0, 30, 20)));
}

TEST(FlutterMutatorViewTest, FrameWithTightClipAndTransformIsCorrect) {
  NSView* platformView = [[NSView alloc] init];
  FlutterMutatorView* mutatorView = [[FlutterMutatorView alloc] initWithPlatformView:platformView];
  NSView* mutatorViewParent = [[NSView alloc] init];
  mutatorViewParent.wantsLayer = YES;
  mutatorViewParent.layer.contentsScale = 2.0;
  [mutatorViewParent addSubview:mutatorView];

  std::vector<FlutterPlatformViewMutation> mutations{
      {
          .type = kFlutterPlatformViewMutationTypeTransformation,
          .transformation =
              FlutterTransformation{
                  .scaleX = 2,
                  .scaleY = 2,
              },
      },
      {
          .type = kFlutterPlatformViewMutationTypeClipRect,
          .clip_rect = FlutterRect{80, 40, 200, 100},
      },
      {
          .type = kFlutterPlatformViewMutationTypeClipRect,
          .clip_rect = FlutterRect{110, 55, 120, 65},
      },
      {
          .type = kFlutterPlatformViewMutationTypeTransformation,
          .transformation =
              FlutterTransformation{
                  .scaleX = 1,
                  .transX = 100,
                  .scaleY = 1,
                  .transY = 50,
              },
      },
      {
          .type = kFlutterPlatformViewMutationTypeTransformation,
          .transformation =
              FlutterTransformation{
                  .scaleX = 1.5,
                  .transX = -7.5,
                  .scaleY = 1.5,
                  .transY = -5,
              },
      },
  };

  ApplyFlutterLayer(mutatorView, FlutterSize{30 * 2, 20 * 2}, mutations);

  EXPECT_TRUE(CGRectEqualToRect(mutatorView.frame, CGRectMake(110, 55, 10, 10)));
  EXPECT_TRUE(
      CGRectEqualToRect(mutatorView.subviews.firstObject.frame, CGRectMake(-17.5, -10, 45, 30)));
  EXPECT_TRUE(CGRectEqualToRect(platformView.frame, CGRectMake(0, 0, 30, 20)));
}

// Rounded rectangle without hitting the corner
TEST(FlutterMutatorViewTest, RoundRectClipsToSimpleRectangle) {
  NSView* platformView = [[NSView alloc] init];
  FlutterMutatorView* mutatorView = [[FlutterMutatorView alloc] initWithPlatformView:platformView];

  std::vector<FlutterPlatformViewMutation> mutations{
      {
          .type = kFlutterPlatformViewMutationTypeClipRoundedRect,
          .clip_rounded_rect =
              FlutterRoundedRect{
                  .rect = FlutterRect{110, 30, 120, 90},
                  .upper_left_corner_radius = FlutterSize{10, 10},
                  .upper_right_corner_radius = FlutterSize{10, 10},
                  .lower_right_corner_radius = FlutterSize{10, 10},
                  .lower_left_corner_radius = FlutterSize{10, 10},
              },
      },
      {
          .type = kFlutterPlatformViewMutationTypeTransformation,
          .transformation =
              FlutterTransformation{
                  .scaleX = 1,
                  .transX = 100,
                  .scaleY = 1,
                  .transY = 50,
              },
      },
  };

  ApplyFlutterLayer(mutatorView, FlutterSize{30, 20}, mutations);

  EXPECT_TRUE(CGRectEqualToRect(mutatorView.frame, CGRectMake(110, 50, 10, 20)));
  EXPECT_TRUE(
      CGRectEqualToRect(mutatorView.subviews.firstObject.frame, CGRectMake(-10, 0, 30, 20)));
  EXPECT_TRUE(CGRectEqualToRect(platformView.frame, CGRectMake(0, 0, 30, 20)));
  EXPECT_EQ(mutatorView.pathClipViews.count, 0ul);
}

// Ensure that the mutator view, clip views, and container all use a flipped y axis. The transforms
// sent from the framework assume this, and so aside from the consistency with every other embedder,
// we can avoid a lot of extra math.
TEST(FlutterMutatorViewTest, ViewsSetIsFlipped) {
  NSView* platformView = [[NSView alloc] init];
  FlutterMutatorView* mutatorView = [[FlutterMutatorView alloc] initWithPlatformView:platformView];

  std::vector<FlutterPlatformViewMutation> mutations{
      {
          .type = kFlutterPlatformViewMutationTypeClipRoundedRect,
          .clip_rounded_rect =
              FlutterRoundedRect{
                  .rect = FlutterRect{110, 60, 150, 150},
                  .upper_left_corner_radius = FlutterSize{10, 10},
                  .upper_right_corner_radius = FlutterSize{10, 10},
                  .lower_right_corner_radius = FlutterSize{10, 10},
                  .lower_left_corner_radius = FlutterSize{10, 10},
              },
      },
      {
          .type = kFlutterPlatformViewMutationTypeTransformation,
          .transformation =
              FlutterTransformation{
                  .scaleX = 1,
                  .transX = 100,
                  .scaleY = 1,
                  .transY = 50,
              },
      },
  };

  ApplyFlutterLayer(mutatorView, FlutterSize{30, 20}, mutations);

  EXPECT_TRUE(mutatorView.isFlipped);
  ASSERT_EQ(mutatorView.pathClipViews.count, 1ul);
  EXPECT_TRUE(mutatorView.pathClipViews.firstObject.isFlipped);
  EXPECT_TRUE(mutatorView.platformViewContainer.isFlipped);
}

TEST(FlutterMutatorViewTest, RectsClipsToPathWhenRotated) {
  NSView* platformView = [[NSView alloc] init];
  FlutterMutatorView* mutatorView = [[FlutterMutatorView alloc] initWithPlatformView:platformView];
  std::vector<FlutterPlatformViewMutation> mutations{
      {
          .type = kFlutterPlatformViewMutationTypeTransformation,
          // Roation M_PI / 8
          .transformation =
              FlutterTransformation{
                  .scaleX = 0.9238795325112867,
                  .skewX = -0.3826834323650898,
                  .skewY = 0.3826834323650898,
                  .scaleY = 0.9238795325112867,
              },
      },
      {
          .type = kFlutterPlatformViewMutationTypeClipRect,
          .clip_rect = FlutterRect{110, 60, 150, 150},
      },
      {
          .type = kFlutterPlatformViewMutationTypeTransformation,
          .transformation =
              FlutterTransformation{
                  .scaleX = 1,
                  .transX = 100,
                  .scaleY = 1,
                  .transY = 50,
              },
      },
  };
  ApplyFlutterLayer(mutatorView, FlutterSize{30, 20}, mutations);
  EXPECT_EQ(mutatorView.pathClipViews.count, 1ul);
  EXPECT_NEAR(mutatorView.platformViewContainer.frame.size.width, 35.370054622640396, kMaxErr);
  EXPECT_NEAR(mutatorView.platformViewContainer.frame.size.height, 29.958093621178421, kMaxErr);
}

TEST(FlutterMutatorViewTest, RoundRectClipsToPath) {
  NSView* platformView = [[NSView alloc] init];
  FlutterMutatorView* mutatorView = [[FlutterMutatorView alloc] initWithPlatformView:platformView];

  std::vector<FlutterPlatformViewMutation> mutations{
      {
          .type = kFlutterPlatformViewMutationTypeClipRoundedRect,
          .clip_rounded_rect =
              FlutterRoundedRect{
                  .rect = FlutterRect{110, 60, 150, 150},
                  .upper_left_corner_radius = FlutterSize{10, 10},
                  .upper_right_corner_radius = FlutterSize{10, 10},
                  .lower_right_corner_radius = FlutterSize{10, 10},
                  .lower_left_corner_radius = FlutterSize{10, 10},
              },
      },
      {
          .type = kFlutterPlatformViewMutationTypeTransformation,
          .transformation =
              FlutterTransformation{
                  .scaleX = 1,
                  .transX = 100,
                  .scaleY = 1,
                  .transY = 50,
              },
      },
  };

  ApplyFlutterLayer(mutatorView, FlutterSize{30, 20}, mutations);

  EXPECT_TRUE(CGRectEqualToRect(mutatorView.frame, CGRectMake(110, 60, 20, 10)));
  EXPECT_TRUE(
      CGRectEqualToRect(mutatorView.subviews.firstObject.frame, CGRectMake(-10, -10, 30, 20)));
  EXPECT_TRUE(CGRectEqualToRect(platformView.frame, CGRectMake(0, 0, 30, 20)));
  EXPECT_EQ(mutatorView.pathClipViews.count, 1ul);
  ExpectTransform3DEqual(mutatorView.pathClipViews.firstObject.layer.mask.transform,
                         CATransform3DMakeTranslation(-100, -50, 0));
}

TEST(FlutterMutatorViewTest, PathClipViewsAreAddedAndRemoved) {
  NSView* platformView = [[NSView alloc] init];
  FlutterMutatorView* mutatorView = [[FlutterMutatorView alloc] initWithPlatformView:platformView];

  std::vector<FlutterPlatformViewMutation> mutations{
      {
          .type = kFlutterPlatformViewMutationTypeTransformation,
          .transformation =
              FlutterTransformation{
                  .scaleX = 1,
                  .transX = 100,
                  .scaleY = 1,
                  .transY = 50,
              },
      },
  };

  ApplyFlutterLayer(mutatorView, FlutterSize{30, 20}, mutations);

  EXPECT_TRUE(CGRectEqualToRect(mutatorView.frame, CGRectMake(100, 50, 30, 20)));
  EXPECT_EQ(mutatorView.pathClipViews.count, 0ull);

  std::vector<FlutterPlatformViewMutation> mutations2{
      {
          .type = kFlutterPlatformViewMutationTypeClipRoundedRect,
          .clip_rounded_rect =
              FlutterRoundedRect{
                  .rect = FlutterRect{110, 60, 150, 150},
                  .upper_left_corner_radius = FlutterSize{10, 10},
                  .upper_right_corner_radius = FlutterSize{10, 10},
                  .lower_right_corner_radius = FlutterSize{10, 10},
                  .lower_left_corner_radius = FlutterSize{10, 10},
              },
      },
      {
          .type = kFlutterPlatformViewMutationTypeTransformation,
          .transformation =
              FlutterTransformation{
                  .scaleX = 1,
                  .transX = 100,
                  .scaleY = 1,
                  .transY = 50,
              },
      },
  };

  ApplyFlutterLayer(mutatorView, FlutterSize{30, 20}, mutations2);

  EXPECT_TRUE(CGRectEqualToRect(mutatorView.frame, CGRectMake(110, 60, 20, 10)));
  EXPECT_TRUE(
      CGRectEqualToRect(mutatorView.subviews.firstObject.frame, CGRectMake(-10, -10, 30, 20)));
  EXPECT_TRUE(CGRectEqualToRect(platformView.frame, CGRectMake(0, 0, 30, 20)));
  EXPECT_EQ(mutatorView.pathClipViews.count, 1ul);

  EXPECT_EQ(platformView.superview, mutatorView.platformViewContainer);
  EXPECT_EQ(mutatorView.platformViewContainer.superview, mutatorView.pathClipViews[0]);
  EXPECT_EQ(mutatorView.pathClipViews[0].superview, mutatorView);

  std::vector<FlutterPlatformViewMutation> mutations3{
      {
          .type = kFlutterPlatformViewMutationTypeClipRoundedRect,
          .clip_rounded_rect =
              FlutterRoundedRect{
                  .rect = FlutterRect{110, 55, 150, 150},
                  .upper_left_corner_radius = FlutterSize{10, 10},
                  .upper_right_corner_radius = FlutterSize{10, 10},
                  .lower_right_corner_radius = FlutterSize{10, 10},
                  .lower_left_corner_radius = FlutterSize{10, 10},
              },
      },
      {
          .type = kFlutterPlatformViewMutationTypeClipRoundedRect,
          .clip_rounded_rect =
              FlutterRoundedRect{
                  .rect = FlutterRect{30, 30, 120, 65},
                  .upper_left_corner_radius = FlutterSize{10, 10},
                  .upper_right_corner_radius = FlutterSize{10, 10},
                  .lower_right_corner_radius = FlutterSize{10, 10},
                  .lower_left_corner_radius = FlutterSize{10, 10},
              },
      },
      {
          .type = kFlutterPlatformViewMutationTypeTransformation,
          .transformation =
              FlutterTransformation{
                  .scaleX = 1,
                  .transX = 100,
                  .scaleY = 1,
                  .transY = 50,
              },
      },
  };

  ApplyFlutterLayer(mutatorView, FlutterSize{30, 20}, mutations3);

  EXPECT_TRUE(CGRectEqualToRect(mutatorView.frame, CGRectMake(110, 55, 10, 10)));
  EXPECT_TRUE(
      CGRectEqualToRect(mutatorView.subviews.firstObject.frame, CGRectMake(-10, -5, 30, 20)));
  EXPECT_EQ(mutatorView.pathClipViews.count, 2ul);

  EXPECT_EQ(platformView.superview, mutatorView.platformViewContainer);
  EXPECT_EQ(mutatorView.platformViewContainer.superview, mutatorView.pathClipViews[1]);
  EXPECT_EQ(mutatorView.pathClipViews[1].superview, mutatorView.pathClipViews[0]);
  EXPECT_EQ(mutatorView.pathClipViews[0].superview, mutatorView);

  ApplyFlutterLayer(mutatorView, FlutterSize{30, 20}, mutations2);

  EXPECT_TRUE(CGRectEqualToRect(mutatorView.frame, CGRectMake(110, 60, 20, 10)));
  EXPECT_TRUE(
      CGRectEqualToRect(mutatorView.subviews.firstObject.frame, CGRectMake(-10, -10, 30, 20)));
  EXPECT_TRUE(CGRectEqualToRect(platformView.frame, CGRectMake(0, 0, 30, 20)));
  EXPECT_EQ(mutatorView.pathClipViews.count, 1ul);

  EXPECT_EQ(platformView.superview, mutatorView.platformViewContainer);
  EXPECT_EQ(mutatorView.platformViewContainer.superview, mutatorView.pathClipViews[0]);
  EXPECT_EQ(mutatorView.pathClipViews[0].superview, mutatorView);

  ApplyFlutterLayer(mutatorView, FlutterSize{30, 20}, mutations);

  EXPECT_TRUE(CGRectEqualToRect(mutatorView.frame, CGRectMake(100, 50, 30, 20)));
  EXPECT_EQ(mutatorView.pathClipViews.count, 0ull);
}
